package org.oos.auth;

import static org.oos.auth.AuthConstants.GET_ACCESS_TOKEN_URL;
import static org.oos.auth.AuthConstants.GET_TEMP_TOKEN_URL;

import java.io.IOException;

import org.oos.auth.OosTemporaryTokenOAuthUrl.Privilege;

import com.google.api.client.auth.oauth.OAuthCredentialsResponse;
import com.google.api.client.auth.oauth.OAuthGetAccessToken;
import com.google.api.client.auth.oauth.OAuthGetTemporaryToken;
import com.google.api.client.auth.oauth.OAuthHmacSigner;
import com.google.api.client.http.HttpTransport;

/**
 * 
 * This class is used to provide with OAuth access to 11870.com services. The
 * basic requirements to use it are:
 * <ul>
 * <li>Application Token</li>
 * <li>Application Secret</li>
 * </ul>
 * These can be obtained from http://11870.com/pablisco/apps
 * <p>
 * 
 * </p>
 * 
 * @author pablo.gonzalez@11870.com
 * 
 */
public class OAuthProtocolManager {
	
	private OAuthHmacSigner signer;
	
	/**
	 * a.k.a. APPTOKEN
	 */
	private String consumerKey;
	/**
	 * The secret provided by 11870.com
	 */
	private String consumerSecret;
	/**
	 * The temporary token. This can be temporary or long-lived.
	 */
	private String accessToken = null;
	/**
	 * The secret used by the signed to sign the petitions. It could be
	 * temporary or long lived
	 */
	private String tokenSecret = null;
	/**
	 * URL to be forwarded once the user has validated.
	 */
	private String callbackURL = null;

	private enum State {
		/**
		 * Initial state without any authorization information.
		 */
		Unauthorized,
		/**
		 * There is a temporary token that can be used to request the
		 * authorization url.
		 */
		HasToken,
		/**
		 * The session is authorized and it is safe to sign
		 * {@link HttpTransport} instances
		 */
		Authorized;
	}

	private State currentState = State.Unauthorized;

	/**
	 * This method checks that both appKey and the clientSecret are not null. It
	 * will throw an {@link NullPointerException} if any of them are.
	 * <p>
	 * It also checks for the correct state.
	 * </p>
	 */
	private void checkState(State state) {
		if (consumerKey == null) {
			throw new NullPointerException("OOSAuth needs an application key.");
		}
		if (consumerSecret == null) {
			throw new NullPointerException(
					"OOSAuth needs an application secret.");
		}
		if (currentState != state) {
			throw new IllegalStateException("Current State is\" "
					+ currentState + "\" but \"" + state + "\" was required");
		}
	}

	/**
	 * 
	 * Sets the application's token obtained from 11870.com
	 * 
	 * @param appKey
	 * @return
	 */
	public OAuthProtocolManager setConsumerKey(String appKey) {
		this.consumerKey = appKey;
		return this;
	}

	/**
	 * @param secret
	 *            associated with the provided application token.
	 * @return
	 */
	public OAuthProtocolManager setConsumerSecret(String secret) {
		this.consumerSecret = secret;
		return this;
	}

	/**
	 * @param callbackURL
	 *            The URL to be forwarded when the authorization is complete.
	 *            This has to be set before the temporary token is retrieved.
	 * @return
	 */
	public OAuthProtocolManager setCallback(String callbackURL) {
		this.callbackURL = callbackURL;
		return this;
	}

	/**
	 * @return current token either provided by the server or manually.
	 */
	public String getAccessToken() {
		return accessToken;
	}

	/**
	 * @return the secret associated with current token.
	 */
	public String getTokenSecret() {
		return tokenSecret;
	}

	/**
	 * 
	 * Replaces the temporary token and its secret with the ones provided by
	 * the {@link OAuthCredentialsResponse}
	 * 
	 * @param response
	 */
	private boolean processTempTokenResponse(OAuthCredentialsResponse response) {
		if (response.token != null && response.tokenSecret != null) {
			this.accessToken = response.token;
			this.tokenSecret = response.tokenSecret;
			return true;
		} else {
			return false;
		}
	}

	/**
	 * 
	 * Requests a temporary token to the server. This can only be used if the
	 * state is unauthorized. To return to the initial state use clear().
	 * 
	 * @return
	 * @throws IOException
	 */
	public synchronized String requestTemporaryToken() throws IOException {
		checkState(State.Unauthorized);
		OAuthGetTemporaryToken getTemporaryToken = new OAuthGetTemporaryToken(GET_TEMP_TOKEN_URL);
		getTemporaryToken.signer = getSigner();
		getTemporaryToken.consumerKey = consumerKey;
		if (callbackURL != null) {
			getTemporaryToken.callback = callbackURL;
		}
		// change state and return current temporary token
		if (processTempTokenResponse(getTemporaryToken.execute())) {
			currentState = State.HasToken;
			return accessToken;
		}
		return null;
	}

	/**
	 * 
	 * This method provides with an authorization URL to be used in a web
	 * browser so that the user can be asked for authorization.
	 * 
	 * @return
	 * @throws IOException
	 */
	public String getAuthorizeUrl() throws IOException {
		return getAuthorizeUrl(Privilege.Read);
	}

	/**
	 * 
	 * Same as getAuthorizeUrl but allowing to set the privileges that are asked
	 * to the user.
	 * 
	 * @param privilege
	 * @return
	 * @throws IOException
	 */
	public String getAuthorizeUrl(Privilege privilege) throws IOException {
		checkState(State.HasToken);
		OosTemporaryTokenOAuthUrl oosAuthorizeUrl = new OosTemporaryTokenOAuthUrl();
		oosAuthorizeUrl.setPrivilege(privilege);
		oosAuthorizeUrl.temporaryToken = accessToken;
		return oosAuthorizeUrl.build();
	}

	/**
	 * 
	 * Once the user has authorized the app to access the verifier is needed to
	 * be able to sign the transport.
	 * 
	 * @param verifier
	 * @throws IOException
	 */
	public boolean verifyAccess(String verifier) throws IOException {
		checkState(State.HasToken);
		OAuthGetAccessToken getAccessToken = new OAuthGetAccessToken(GET_ACCESS_TOKEN_URL);
		getAccessToken.temporaryToken = accessToken;
		getAccessToken.signer = getSigner();
		getAccessToken.consumerKey = consumerKey;
		getAccessToken.verifier = verifier;
		boolean success = processTempTokenResponse(getAccessToken.execute());
		if (success) {
			currentState = State.Authorized;
		}
		return success;
	}

//	/**
//	 * 
//	 * This method is used to manually set the OOSAuth as authorized.
//	 * 
//	 * @param token
//	 * @param secret
//	 */
//	public void setManualAuthorization(String token, String secret) {
//		checkState(State.Unauthorized);
//		this.accessToken = token;
//		this.tokenSecret = secret;
//		currentState = State.Authorized;
//	}
//
//	/**
//	 * 
//	 * Once the {@link OosAuth} is verified or the relevant data has been
//	 * provided then this method is used to sign the transport that will be used
//	 * to do the Atom requests.
//	 * 
//	 * @param transport
//	 */
//	public HttpTransport signTransport(HttpTransport transport) {
//		checkState(State.Authorized);
//		OAuthParameters authorizer = new OAuthParameters();
//		authorizer.consumerKey = consumerKey;
//		authorizer.signer = getSigner();
//		authorizer.token = accessToken;
//		authorizer.signRequestsUsingAuthorizationHeader(transport);
//		return transport;
//	}

	/**
	 * 
	 * Provides with the signer and if the signer is already created then update
	 * the client and the token shared secrets (if they exist).
	 * 
	 * @return
	 */
	public OAuthHmacSigner getSigner() {
		if (signer == null){
			signer = new OAuthHmacSigner();
			signer.clientSharedSecret = consumerSecret;
			signer.tokenSharedSecret = tokenSecret;
		}
		return signer;
	}

	/**
	 * Returns the {@link OAuthProtocolManager} to it's initial state, with all values
	 * removed except for the application token and secret.
	 */
	public void clear() {
		currentState = State.Unauthorized;
		accessToken = null;
		tokenSecret = null;
		callbackURL = null;
	}

	/**
	 * @return True if the {@link OAuthProtocolManager} has been authorized.
	 */
	public boolean isAuthorized() {
		return currentState == State.Authorized;
	}

}
